Skip to content

Show worktrees in the repository sidebar#105

Merged
pol-rivero merged 5 commits into
pol-rivero:mainfrom
ignatremizov:feat/sidebar-worktrees
Apr 4, 2026
Merged

Show worktrees in the repository sidebar#105
pol-rivero merged 5 commits into
pol-rivero:mainfrom
ignatremizov:feat/sidebar-worktrees

Conversation

@ignatremizov

@ignatremizov ignatremizov commented Mar 26, 2026

Copy link
Copy Markdown

Summary

Add an optional sidebar mode that shows linked worktrees nested under their repository in the main repository list.

This keeps repository switching fast for worktree-heavy workflows without forcing users through the worktree dropdown for every switch.

Changes

  • add a secondary Appearance setting to show worktrees in the repository sidebar when worktree support is enabled
  • group linked worktrees under their main repository in the sidebar
  • synthesize child rows for linked worktrees discovered from git worktree list even when those worktrees were never added as repositories
  • support linked-only setups by synthesizing sibling worktree rows even when the stored entry is itself a linked worktree instead of the main worktree
  • use worktree folder names for child row labels while preserving existing alias styling for saved repository entries
  • use the same displayed-title logic for sorting and disambiguation so nested rows sort and label consistently with what the user sees
  • preload main-repository worktree state for the sidebar so nested rows and stored linked-worktree branch pills are available on initial render instead of only after opening the worktree dropdown or forcing another sidebar refresh
  • refresh parent sidebar rows when linked worktrees are selected so nested rows stay in sync with the active repository view
  • let explicit parent-row clicks in the sidebar open the main worktree instead of restoring the last selected linked worktree from the preferred-worktree flow added in store last selected worktree, and open that when reselecting repo #106
  • surface nested worktree rows from both saved worktree repositories and synthetic virtual rows without duplicating entries already stored in Desktop
  • avoid duplicate Pull all work for linked worktrees while still including orphan linked worktrees when the main repo is absent from the stored repository list
  • route virtual worktree open failures through the normal app error path instead of silently failing
  • open synthetic worktree rows transiently instead of persisting them as top-level repositories under Other
  • keep linked worktree rows as leaf nodes in the sidebar, regardless of whether they come from saved repositories or synthetic worktree discovery
  • replace Remove… with Delete… for linked worktree rows and make deleting a saved linked worktree remove both the Desktop repository entry and the on-disk worktree/Git worktree metadata
  • remove group-name actions from saved linked worktree rows while keeping alias support for explicitly-added saved worktrees
  • inherit the main repository's GitHub association when a known linked worktree is added through Add local repository, so saved worktrees stay grouped under the main repo instead of moving to Other
  • ignore stash-metric persistence for transient synthetic rows so sidebar-only worktree selection does not try to look up non-persisted repositories in repositories-store
  • keep transient synthetic worktree selections stable across repository-store refreshes so switching to a worktree from another repository family does not snap back to the previously selected saved repository
  • add stale worktree affordances in the sidebar with a warning icon, clearer tooltip text, click suppression, and a context-menu action that prunes stale worktree metadata for the repository
  • avoid an empty leading context-menu section for virtual rows by only rendering the alias/group separator when those actions are present
  • make the Add local repository dialog submission reliable for linked worktree paths by resolving the submitted path consistently and waiting for picker-selected paths to land in component state before submit runs
  • persist sidebar worktree metadata in repository state and extract the sidebar-specific state shaping into a dedicated helper to keep app-store.ts smaller
  • move the worktree-specific sidebar row construction into a dedicated helper so group-repositories.ts stays closer to upstream
  • throttle sidebar worktree refreshes during repository indicator updates to reduce repeated git worktree list churn
  • prune sidebar worktree refresh timestamps when repository lists change so stale cache entries do not accumulate
  • tighten TypeScript null/undefined handling in repository and sidebar list code so production webpack builds pass across the full CI matrix
  • add and extend unit coverage for grouped rows, synthetic rows, linked-only setups, stored linked-worktree branch labels, stale worktree rows, repository-list context menu behavior, and transient synthetic repository store behavior

Notes

This builds on the existing worktree support in #62, the toolbar hierarchy follow-up in #81, and the preferred-worktree persistence added in #106.

The feature stays behind a separate Appearance setting so users can keep the existing grouped-in-dropdown workflow if they prefer it.

The sidebar intentionally supports both kinds of linked worktrees:

  • repositories that were already added to Desktop as standalone entries
  • worktrees that only exist in Git metadata and were never added through Desktop

That second case is why the implementation includes synthetic child rows and startup/sidebar refresh state, rather than only changing the existing filter logic.

Because #106 persists the last selected worktree for a repository, this PR also makes explicit parent-row clicks in the sidebar opt out of that restore path so clicking the repository row still opens the main worktree directly.

Linked worktrees are now always treated as leaf nodes in the sidebar, even when they were explicitly added to Desktop as saved repositories. Only the main worktree row owns nested worktree children.

Saved linked worktrees still keep alias support, but group-name actions are removed for them to avoid implying that linked worktrees can act as group parents. Synthetic worktree alias support would require a separate metadata store keyed by worktree path, so that is left as possible follow-up work instead of extending the normal repository persistence model in this PR.

For stale worktrees, the new prune action is repository-scoped because it maps to git worktree prune, which removes any stale worktree metadata Git reports for that repository.

Testing

  • yarn test:unit app/test/unit/repositories-list-grouping-test.ts
  • yarn test:unit app/test/unit/repository-list-item-context-menu-test.ts
  • yarn test:unit app/test/unit/repositories-store-test.ts
  • yarn lint
  • yarn compile:dev
  • yarn compile:prod

Example

image

New setting if "show worktrees dropdown in toolbar" is ticked

image

Stale worktrees

image image image

@pol-rivero

Copy link
Copy Markdown
Owner

I think this is a much needed feature, but I'm not a big fan of the large diff in app-store.ts. Maybe you could have a look at this block:

const repositories = this.state.repositories.filter(
    r => !(r instanceof Repository && isLinkedWorktreeSync(r.path))
  )

This was added here: 546af41. Conditionally skipping this filter should be enough to show linked workspaces in the sidebar, I think.

@ignatremizov

ignatremizov commented Mar 27, 2026

Copy link
Copy Markdown
Author

Sure, give me a day to take a look

This was mostly one-shot with codex, without the usual review hardening passes I do since I just wanted to get it working for myself

The issue with that filter is that worktrees could be created but not saved in Github Desktop, or already saved in Github Desktop via the "add local repository" action:

image

But the idea is to generate synthetic rows for the worktrees that don't exist as saved yet, while also handling the ones that are saved as "local repos"

the original implementation merged decided a dropdown table per "real" repo is better, and to hide any earlier repos which are themselves worktrees, while mine reverses that decision and renders the worktrees with synthetic repo rows

@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch 20 times, most recently from 0cd4fad to a7260a0 Compare March 30, 2026 13:08
@pol-rivero

pol-rivero commented Mar 30, 2026

Copy link
Copy Markdown
Owner

The issue with that filter is that worktrees could be created but not saved in Github Desktop, or already saved in Github Desktop via the "add local repository" action.

Ah I see, that makes sense.

Edit: I've done an initial pass on your PR. While it's still bigger than I'd like, most of the complexity seems justified, I haven't found any good workaround to avoid creating those synthetic rows.
I've added some comments below to try and mitigate the large complexity slightly.

Comment thread app/src/lib/git/worktree.ts Outdated
Comment thread app/src/lib/git/worktree.ts Outdated
Comment thread .github/actions/setup-ci-environment/action.yml Outdated
Comment thread app/src/ui/repositories-list/group-repositories.ts Outdated
@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch 9 times, most recently from 8a87499 to 470f135 Compare April 1, 2026 13:15
@ignatremizov

Copy link
Copy Markdown
Author

Added:

  • open synthetic worktree rows transiently instead of persisting them as top-level repositories under Other
  • keep stored linked-worktree rows on the normal repository-management context menu while giving synthetic rows a worktree-specific context menu
  • add stale worktree affordances in the sidebar with a warning icon, clearer tooltip text, click suppression, and a context-menu action that prunes stale worktree metadata for the repository
  • avoid an empty leading context-menu section for virtual rows by only rendering the alias/group separator when those actions are present

@ignatremizov

Copy link
Copy Markdown
Author

@pol-rivero check if anything else is missing or you want moved around

@pol-rivero

pol-rivero commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Thanks for the update. I've done some more tests and found the following:

  • While the context menu in the synthetic rows seems correct, the non-synthetic worktrees still show the old context menu (see comment).
  • When clicking on a synthetic row, the repository seems to be in an inconsistent state with negative ids
image

Also, I'd appreciate it if you could do separate commits instead of just updating a single commit, this way I can see the actual changes between revisions.

@ignatremizov

ignatremizov commented Apr 2, 2026

Copy link
Copy Markdown
Author

@pol-rivero I pushed a follow-up fix for the synthetic-row state issue.

Root cause was not the negative ids by themselves, but that synthetic sidebar-only worktree rows are transient Repository objects and are not persisted in repositories-store. Some stash-metric code still assumed every selected repository existed in that store, which is what caused the getLastStashCheckDate failure for the synthetic row.

I fixed that by treating transient synthetic rows as non-persisted in the stash-check tracking path, and added a regression test for it.

On the context menu behavior: I intentionally kept stored linked-worktree rows on the normal repository menu for now. My reasoning is that if a user explicitly added a worktree as a regular Desktop repository, they should still be able to use the normal repo actions there (alias, grouping, etc), especially Remove…, which updates Desktop’s stored repository list. Once that stored row is removed, the synthesized sidebar row becomes the representation for that worktree, and that row uses the worktree-specific context menu.

That keeps the behavior compatible with the base GitHub Desktop mental model instead of automatically "converting" stored repo rows to worktrees, or hiding them entirely.

If you’d prefer a different UX here, I can revisit that part separately, but I wanted to avoid implicitly changing persisted repo rows into a different interaction model, even if they are semantically worktrees.

Add an optional sidebar mode that shows linked worktrees nested under their repository in the main repository list so repository switching can stay in the main sidebar instead of requiring the worktree dropdown.

Changes:
- add a secondary Appearance setting to show worktrees in the repository sidebar when worktree support is enabled
- group linked worktrees under their main repository in the sidebar
- synthesize child rows for linked worktrees discovered from `git worktree list` even when those worktrees were never added as repositories
- support linked-only setups by synthesizing sibling worktree rows even when the stored entry is itself a linked worktree instead of the main worktree
- use worktree folder names for child row labels while preserving existing alias styling for saved repository entries
- use the same displayed-title logic for sorting and disambiguation so nested rows sort and label consistently with what the user sees
- preload main-repository worktree state for the sidebar so nested rows and stored linked-worktree branch pills are available on initial render instead of only after opening the worktree dropdown or forcing another sidebar refresh
- refresh parent sidebar rows when linked worktrees are selected so nested rows stay in sync with the active repository view
- surface nested worktree rows from both saved worktree repositories and synthetic virtual rows without duplicating entries already stored in Desktop
- avoid duplicate `Pull all` work for linked worktrees while still including orphan linked worktrees when the main repo is absent from the stored repository list
- route virtual worktree open failures through the normal app error path instead of silently failing
- open synthetic worktree rows transiently instead of persisting them as top-level repositories under `Other`
- keep stored linked-worktree rows on the repository-management context menu while giving synthetic rows a worktree-specific context menu that uses `PopupType.DeleteWorktree`
- persist sidebar worktree metadata in repository state and extract the sidebar-specific state shaping into a dedicated helper to keep `app-store.ts` smaller
- throttle sidebar worktree refreshes during repository indicator updates to reduce repeated `git worktree list` churn
- prune sidebar worktree refresh timestamps when repository lists change so stale cache entries do not accumulate
- tighten TypeScript null/undefined handling in repository and sidebar list code so production webpack builds pass across the full CI matrix
- add and extend unit coverage for grouped rows, synthetic rows, linked-only setups, stored linked-worktree branch labels, and repository-list context menu behavior

Behavioral effect:
Users can opt into seeing and switching linked worktrees directly from the main repository sidebar, including unstored Git worktrees, with branch labels, parent-child grouping, and worktree-aware context menu behavior available without forcing those virtual rows into the saved repository list.

Testing:
- yarn test:unit app/test/unit/repositories-list-grouping-test.ts
- yarn test:unit app/test/unit/repository-list-item-context-menu-test.ts
- yarn lint
- yarn compile:dev
- yarn compile:prod
@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch from 44f87b6 to a229645 Compare April 2, 2026 19:07
…ktree rows

Synthetic sidebar worktree rows are transient `Repository` objects created only for sidebar navigation and they are not persisted in `repositories-store`. Stash metric collection still assumed every selected repository existed in the store, which caused fatal lookup failures when a synthetic row became active.

Changes:
- short-circuit stash-check read/write helpers for transient repositories with negative ids instead of querying the repositories database
- keep persisted repositories on the existing stash metric path unchanged
- add a repositories-store regression test covering synthetic sidebar rows so transient worktree selection cannot reintroduce the fatal lookup path

Behavioral effect:
Selecting a synthetic sidebar worktree row no longer throws `getLastStashCheckDate` errors or leaves the app in an inconsistent state just because the row is transient and not saved in Desktop's repository store.

Testing:
- yarn test:unit app/test/unit/repositories-store-test.ts
- yarn eslint app/src/lib/stores/repositories-store.ts app/test/unit/repositories-store-test.ts
- yarn compile:prod
Upstream now restores the last selected linked worktree when reselecting a repository, which is useful for normal repo switching but conflicts with the sidebar parent-row interaction. After visiting a synthetic child row, clicking the parent row should open the parent repository itself instead of bouncing back into the remembered worktree.

Changes:
- add an explicit `followPreferredWorktree` selection flag through dispatcher and app-store repository selection paths
- keep the upstream preferred-worktree restore behavior enabled by default for existing selection flows
- make repository sidebar row clicks opt out of preferred-worktree restoration so an explicit parent-row click opens the main worktree

Behavioral effect:
When a user clicks a repository parent row in the sidebar after visiting a linked worktree, the app now opens the parent repository directly instead of restoring the previously selected child worktree behind the user's back.

Testing:
- yarn eslint app/src/ui/dispatcher/dispatcher.ts app/src/lib/stores/app-store.ts app/src/ui/app.tsx
- yarn compile:prod
@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch from bf73cff to 43a2ddc Compare April 2, 2026 21:56
@pol-rivero

pol-rivero commented Apr 3, 2026

Copy link
Copy Markdown
Owner

@ignatremizov It seems this is still under active development. Let me know once you have a definitive and completely tested version that I can review. :)

Due to the force-pushing, it's very exhausting to continue re-reviewing the same changes all the time.

@pol-rivero

pol-rivero commented Apr 3, 2026

Copy link
Copy Markdown
Owner

I did a quick test for the stash-check and it seems to be fixed. Nice job.

if a user explicitly added a worktree as a regular Desktop repository, they should still be able to use the normal repo actions there (alias, grouping, etc),

In that case, the alias functionality should be fixed, it seems to be broken for linked worktrees at the moment.
I think the grouping functionality should only be shown in the main worktree, because it breaks the worktrees grouping otherwise:

image image

especially Remove…, which updates Desktop’s stored repository list

The "Delete worktree" dialog should also update the stored repository list. If it doesn't, let me know and I'll fix it, because that would be a bug.

I believe that treating linked worktrees as repositories should be an implementation detail, and users should not be aware of it. That's why the context menu on real or synthetic linked worktrees should be identical in my opinion.
Selecting "Remove..." on a linked worktree and being prompted to "Remove repository" seems unintuitive.

@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch from d9c0beb to fa07a22 Compare April 3, 2026 12:39
…behavior

Tighten the follow-up linked-worktree behavior so saved linked worktrees stay attached to their main repository, remain leaf rows, and do not lose selection state when synthetic sidebar rows are active.

Changes:
- keep linked worktree rows as leaf nodes so only main worktrees own nested children in the sidebar
- use `Delete…` for both saved and synthetic linked worktree rows, while removing group-name actions from saved linked worktree rows
- ensure deleting a saved linked worktree also removes its Desktop repository entry so the stored list stays in sync with Git worktree deletion
- inherit the main repository's GitHub association when a linked worktree is added through the local repository flow and its main worktree is already known to Desktop
- keep saved linked worktrees in the same top-level group as their main repository after restart instead of dropping them into `Other`
- preserve transient synthetic worktree selections across repository-store updates so selecting a worktree from another repository family does not snap back to the previously selected saved repository
- keep alias support for saved linked worktrees while treating synthetic rows as transient sidebar-only entries
- retain the regression coverage for orphan linked rows staying flat and for the linked-worktree context menu behavior

Behavioral effect:
Linked worktrees now behave consistently as leaf rows whether they are saved or synthetic. Saved linked worktrees stay grouped under their real main repository, deleting a saved linked worktree updates both Git and Desktop state, and synthetic worktree selection no longer jumps back to an unrelated repository when the saved repository list refreshes.

Testing:
- yarn test:unit app/test/unit/repositories-list-grouping-test.ts
- yarn test:unit app/test/unit/repository-list-item-context-menu-test.ts
- yarn eslint app/src/ui/repositories-list/worktree-list-items.ts app/src/ui/repositories-list/repository-list-item-context-menu.ts app/src/ui/repositories-list/repositories-list.tsx app/src/ui/worktrees/delete-worktree-dialog.tsx app/src/ui/app.tsx app/src/models/popup.ts app/src/lib/stores/app-store.ts app/test/unit/repositories-list-grouping-test.ts app/test/unit/repository-list-item-context-menu-test.ts
- yarn compile:prod
@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch from 9180639 to 7aae8cf Compare April 3, 2026 13:17
Tighten the Add Local Repository dialog so choosing a valid repository path and immediately pressing Add consistently runs the add flow instead of silently stalling in the dialog.

Changes:
- resolve the entered local path before submit-time validation so `validatePath(...)` uses the same normalized path handling as the live `onPathChanged(...)` validation path
- wait for the folder-picker path to finish landing in component state before allowing the chosen path to be used for submission
- keep the rest of the add-existing-repository flow unchanged so successful submissions still dismiss the dialog, add the repository, and select it

Behavioral effect:
The Add Local Repository dialog no longer ends up in a no-op state where a repository path appears valid in the picker flow but submit-time validation never reaches `_addRepositories(...)`. Choosing a worktree path from the folder picker and immediately pressing Add now reliably adds the repository.

Testing:
- yarn eslint app/src/ui/add-repository/add-existing-repository.tsx
- yarn compile:prod
@ignatremizov ignatremizov force-pushed the feat/sidebar-worktrees branch from 7665f78 to cf8bcb6 Compare April 3, 2026 13:30
@ignatremizov

Copy link
Copy Markdown
Author

@pol-rivero I pushed another follow-up based on your feedback.

Changes in this round:

  • linked worktrees are now always treated as leaf rows in the sidebar
  • only the main worktree can own/show nested worktree children
  • saved linked worktrees no longer show group-name actions
  • linked worktree rows now use Delete… instead of Remove…
  • deleting a saved linked worktree now does both:
    • removes it from Desktop’s stored repository list
    • deletes the worktree itself from disk / Git worktree metadata
  • saved linked worktrees now inherit the main repository’s GitHub association when added through “Add local repository”, so they stay grouped under the main repo instead of falling into Other
  • synthetic linked-worktree selections are now kept stable across repository-store updates, so selecting a worktree from another repo family no longer snaps back to the previously selected saved repository
  • the “Add local repository” dialog got two reliability fixes:
    • submit-time validation now resolves the path the same way as the live picker validation
    • picker-selected paths now finish landing in component state before submit runs

I agree for the behavior to be aligned more closely with “linked worktrees are an implementation detail” rather than a second repository model in the UI.

I kept alias support for saved linked worktrees for now. I did not add alias support for synthetic rows in this PR, because that would require introducing a separate store keyed by worktree path instead of relying on the normal repository persistence model.

My current thinking is that synthetic alias support is probably follow-up work if we still want it after merge. In practice, renaming the worktree folder itself already gives a reasonable alias-like result for synthetic rows, since those rows display the basename when no explicit alias exists. If it's still important, I’d prefer to handle that in a separate PR with a small dedicated metadata store rather than growing the repository persistence model further.

About the force-push: I had to rebase after #106 landed because this branch needed to opt explicit sidebar parent-row clicks out of the new "reopen last opened worktree" behavior.

@pol-rivero

pol-rivero commented Apr 4, 2026

Copy link
Copy Markdown
Owner

Phenomenal job! Let's get this merged into the main branch so I can start daily-driving it in search of bugs. :)
Feel free to submit follow-up PRs if you find errors or potential improvements.

In practice, renaming the worktree folder itself already gives a reasonable alias-like result for synthetic rows, since those rows display the basename when no explicit alias exists.

I completely agree, renaming a worktree is enough. That's why I originally proposed not showing the alias option in linked worktrees (synthetic or otherwise).
In fact, I'm still in favor of never showing this option in linked worktrees (even if non-synthetic ones technically support it), because the end user has no idea that some rows are synthetic, and it feels weird to have the option in some worktrees but not in others.

@pol-rivero pol-rivero merged commit c56967c into pol-rivero:main Apr 4, 2026
18 checks passed
@pol-rivero

pol-rivero commented Apr 17, 2026

Copy link
Copy Markdown
Owner

This is now available in v3.5.8

zhangfeiran pushed a commit to zhangfeiran/github-desktop-plus that referenced this pull request Jun 16, 2026
This will be reimplemented later on top of the new worktrees data model.

Reverts c56967c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants